当前位置: GO > golang 继承与组合

golang 继承与组合

2023-11-23 分类:GO 作者:admin 阅读(27)

在前面两篇教程中,学院君已经介绍了 Go 语言不像 Java、PHP 等支持面向编程的语言那样,支持 class 之类的关键字来定义类,而是通过 type 关键字结合基本类型或者结构体来自定义类型系统,此外,它也不支持通过 extends 关键字来显式定义类型之间的继承关系。

所以,严格来说,Go 语言并不是一门面向对象编程语言,至少不是面向对象编程的最佳选择(Java 才是最根正苗红的),不过我们可以基于它提供的一些特性来模拟实现面向对象编程。

要实现面向对象编程,就必须实现面向对象编程的三大特性:封装、继承和多态。

封装

首先是封装,这一点我们在上篇教程中已经详细介绍过:将函数定义为归属某个自定义类型,这就等同于实现了类的成员方法,如果这个自定义类型是基于结构体的,那么结构体的字段可以看做是类的属性。

继承

然后是继承,Go 虽然没有直接提供继承相关的语法实现,但是我们通过组合的方式间接实现类似功能,所谓组合,就是将一个类型嵌入到另一个类型,从而构建新的类型结构。

传统面向对象编程中,显式定义继承关系的弊端有两个:一个是导致类的层级越来越复杂,另一个是影响了类的扩展性,很多软件设计模式的理念就是通过组合来替代继承提高类的扩展性。

我们来看一个例子,现在有一个 Animal 结构体类型,它有一个属性 Name 用于表示该动物的名称,以及三个成员方法,分别用来获取动物叫声、喜欢的食物和动物的名称:


 

如果我们要定义一个继承自该类型的子类 Dog,可以这么做:


这里,我们在 Dog 结构体类型中,嵌入了 Animal 这个类型,这样一来,我们就可以在 Dog 实例上访问所有 Animal 类型包含的属性和方法:


注意这里animal := Animal{"中华田园犬"},并没有指定Name,这是按照属性顺序传递的,推荐直接指定Name

上述代码的打印结果如下:

中华田园犬
动物的叫声...
爱吃的食物...

这就相当于通过组合实现了类与类之间的继承功能。

多态

此外,我们还可以通过在子类中定义同名方法来覆盖父类方法的实现,在面向对象编程中这一术语叫做方法重写,比如在上述 Dog 类型中,我们可以重写 Call 方法和 FavorFood 方法的实现如下:


当我们再执行 main 函数时,直接在 Dog 实例上调用 Call 方法或 FavorFood 方法时,调用的就是 Dog 类中定义的方法而不是 Animal 中定义的方法:

-w681

当然,你可以可以像这样继续调用父类 Animal 中的方法:

fmt.Print(dog.Animal.Call())
fmt.Println(dog.Call())
fmt.Print(dog.Animal.FavorFood())
fmt.Println(dog.FavorFood())

只不过 Go 语言不同于 Java、PHP 等面向对象编程语言,没有专门提供引用父类实例的关键字罢了(super、parent 等),在 Go 语言中,设计哲学一切从简,没有一个多余的关键字,所有的调用都是所见即所得。

这种同一个方法在不同情况下具有不同的表现方式,就是多态,在传统面向对象编程中,多态还有另一个非常常见的使用场景 —— 类对接口的实现,Go 语言也支持此功能,关于这一块我们放到后面接口部分单独介绍。

更多细节

可以看到,与传统面向对象编程语言的继承机制不同,这种组合的实现方式更加灵活,我们不用考虑单继承还是多继承,你想要继承哪个类型的方法,直接组合进来就好了。

多继承同名方法冲突处理

需要注意组合的不同类型之间包含同名方法,比如 Animal 和 Pet 都包含了 GetName 方法,如果子类 Dog 没有重写该方法,直接在 Dog 实例上调用的话会报错:


 

执行上述代码会报错:

# command-line-arguments
chapter04/03-compose.go:49:17: ambiguous selector dog.GetName

除非你显式指定调用哪个父类的方法:

fmt.Println(dog.Pet.GetName())

调整组合位置改变内存布局

另外,我们还可以通过任意调整被组合类型的位置来改变类的内存布局:

type Dog struct {
Animal
Pet
}

type Dog struct {
Pet
Animal
}

虽然上面两个 Dog 子类的功能一致,但是它们的内存结构不同。

继承指针类型的属性和方法

当然,在 Go 语言中,你还可以以指针方式继承某个类型的属性和方法:

type Dog struct {
*Animal
Pet
}

这种情况下,除了传入 Animal 实例的时候要传入指针引用之外,其它调用无需修改:

func main() {
animal := Animal{"中华田园犬"}
pet := Pet{"宠物狗"}
dog := Dog{&animal, pet}
fmt.Println(dog.Animal.GetName())
fmt.Print(dog.Animal.Call())
fmt.Println(dog.Call())
fmt.Print(dog.Animal.FavorFood())
fmt.Println(dog.FavorFood())
}

当我们通过组合实现类之间的继承时,由于结构体实例本身是值类型,如果传入值字面量的话,实际上传入的是结构体实例的副本,对内存耗费更大,所以组合指针类型性能更好。

为组合类型设置别名

前面的示例调用父类方法时都直接引用的是组合类型(父类)的类型字面量,其实,我们还可以像基本类型一样,为其设置别名,方便引用:


 

关于 Go 语言如何通过组合实现类与类之间的继承和方法重写,学院君就简单介绍到这里,下篇教程,我们一起来看看 Go 语言是如何管理类属性和方法的可见性的。

来源:https://geekr.dev/posts/go-oop-with-type-composite


Go 不是一个(传统的)面向对象语言,尽管通过各种奇技淫巧可以实现 OO 的编程风格。

我不赞成「如何在 A 实现 B」之类的尝试。 每个东西都有它自己的特点,这个特点用好了就是优点,用不好就是缺点。非要用汽车拉磨或用驴子拉货,何必呢。

继承 vs 组合

一句话解释,继承是「is sth」,组合是「has sth」。Go 采用组合完美契合了它鸭子类型(duck typing)的设计理念。

“当看到一只鸟走起来像鸭子、游泳起来像鸭子、叫起来也像鸭子,那么这只鸟就可以被称为鸭子。

鸭子类型中,我们重点关注对象能做什么,而不在意它究竟是什么。

对这个理念我略有感触。曾经在 Kotlin (java) 开发中遇到过这样的问题:
第三方包中有个类,没有抽象出接口,我恰恰需要扩展这个东西。于是只好自己定义一个接口,然后写个代理类或者用其他奇奇怪怪的方法达成目的。
你看,它明明是我接口的实现,仅仅因为缺少 implements 关键字,我就得大费周章。

在鸭子类型中这个问题不复存在。

组合要比继承灵活得多。比如 java 中不能让「卡车」既继承「车」又继承「货运工具」,这又偏偏是显示情况。你不能建模为「车 <- 货运工具 <- 卡车」,因为货运工具也可能是飞机。而组合可以轻松办到:


组合绝非继承

本质是语法糖

一些博客会把下面两种写法等价:

它们用起来确实很类似,都可以通过“子类”直接访问“父类”的属性 Dog.name,但这两个有本质差别:

对于 java,name 确实是 Dog 的属性,不可以 Dog.Animal.name 这样来访问。可对于 Go,Dog 是没有 name 属性的。Dog.name 只是一个 Dog.Animal.name 的语法糖。 实际上 Dog 中有一个类型为 Animal 的变量(默认变量名与类型一致),name 依然只属于 Animal。为了更加明显,我们可以给这个变量指定名字:

对象只有一个类型

有人要说了,管它本质是啥,能用不就完事了么。可惜,你用不了… 来看看下面一种典型错误:

在 java 中,因为有继承,Dog 也是一个 Animal,因此这么传参毫无问题。不过在 Go 中,对象只能有一个类型——是 Dog 了就不能是 Animal。Dog 的确包含 Animal 但它还是 dog。就好像,汽车包含轮子,它还叫汽车,不能管它叫轮子。

建模思路

道理我都懂,可还是觉得奇怪 🤨

那是因为我们的命名太有误导性,或者说,建模思路就错了。我们随手就能写出

这样的例子,我想没人会这么写:

很显然,仅管「轮子」比「车」更底层,但它们没有继承关系。

而在 Go 中,用「组合」的思想,把后者实现一遍:

诶,「车拥有轮子」,是不是通顺多了 🥳 既然 java 中行不通的思路在 Go 里毫无违和感,那反过来,把 java 里的常规思路按照所谓的“等价写法”放在 Go 里呢?「猫拥有动物」「货车拥有车」???🧐

由此可见,Go 的设计与传统面向对象完全不同。我们也不能把之前的 OOP 思路强行套在 Go 的开发中。更不应该去找什么「等价写法」。

Go 是组合而非继承,因此在建模过程中我们得 摒弃层级观念,把线性结构转为换网状结构。 比如 人 <- 教师 <- 地理教师 可以转换为 地理教师 consist of(人,地理,教师)。

参数传递

建模完毕,使用中少不了传参。Go 没有继承,自然也就不能「定义父类型形参,传子类型对象」了。解决办法有两种。

直接传“子类型”

最粗暴的方案。

缺点是会丢失额外信息。feed() 无法恢复 a 为 Dog 做进一步处理。

定义接口

这个需求正是接口要做的。

如果 AnimalBaseInfo 字段较多,实现接口是需要写很多方法,那么可以把它们用一个 struct 表示:


来源:https://chenhe.me/post/inheritance-in-go/

「三年博客,如果觉得我的文章对您有用,请帮助本站成长」

赞(0) 打赏

支付宝
微信
0

支付宝
微信
标签:

上一篇:

下一篇:

你可能感兴趣

共有 0 - golang 继承与组合

博客简介

精彩评论

  • admin(6年前 (2020-03-09))

    分别用不同厚度的筏板定义,画图后这设置筏板变截面处理。 http://f.fwxgx.co...

    评:新文章!
  • admin(6年前 (2020-03-09))

    分别用不同厚度的筏板定义,画图后这设置筏板变截面处理。 http://f.fwxgx.co...

    评:新文章!
  • admin(6年前 (2020-03-09))

    新增一个框架图! http://biji.jinli.vip/wp-content/upl...

    评:新文章!
  • 一位WordPress评论者(6年前 (2020-02-13))

    嗨,这是一条评论。 要开始审核、编辑及删除评论,请访问仪表盘的“评论”页面。 评论者头像来自...

    评:世界,您好!